I dataframe sono delle strutture dati equivalenti alle tabelle di un database.
Al giorno d'oggi tutti i linguaggi di alto livello ne hanno un'implementazione.
Un dataframe è una tabella di dati indicizzata da righe e colonne.
il caso più semplice è una tabella che contanga informazioni a proposito di persone;
Ogni riga contiene un'indicativo unico della persona, ogni colonna rappresenta delle caratteristiche di quella persona.
import pandas as pd
data = pd.DataFrame([('Andrea', 24, 178, 'Maschio'),
('Maria', 33, 154, 'Femmina'),
('Luca', 30, 175, 'Maschio')],
columns=['nome', 'età', 'altezza', 'genere'])
data.set_index('nome', inplace=True)
data
import pandas as pd
data = pd.DataFrame([('Andrea', '2015', 'residenza', 'Rimini', 'via stretta 20'),
('Andrea', '2015', 'domicilio', 'Bologna', 'via larga 30'),
('Andrea', '2016', 'residenza', 'Rimini', 'via stretta 20'),
('Andrea', '2016', 'domicilio', 'Bologna', 'via larga 30'),
('Giulio', '2015', 'residenza', 'Rimini', 'via giusta 50'),
('Giulio', '2015', 'domicilio', 'Bologna', 'via falsa 40'),
('Giulio', '2016', 'residenza', 'Bologna', 'via torna 10'),
('Giulio', '2016', 'domicilio', 'Bologna', 'via torna 10'),
], columns=['nome', 'anno', 'tipologia', 'città', 'indirizzo']
)
data.set_index(['nome', 'anno', 'tipologia'], inplace=True)
data = data.unstack()
data.columns = data.columns.swaplevel(0, 1)
data.sortlevel(0, axis=1, inplace=True)
Righe e colonne possono avere indici GERARCHICI, in cui ho più livelli di indicizzazione delle mie informazioni
data
I dataframe permettono di raccogliere e manipolare le informazioni in modo particolarmente comodo, e sono il pilastro centrale della moderna analisi dati.
In particolare hanno una rilevanza centrale i dataframe strutturati come TIDY DATA, termine introdotto da Wickham nel 2010 (paper di spiegazione).
I tidy data sono un modo particolare di strutturare le tabelle che rende l'analisi ed il mantenimento dei dati particolarmente comodo.
La struttura tidy data che Wickham definisce è caratterizzata dalle seguenti proprietà:
Dove:
Corrisponde dal punto di vista formale alla terza forma normale dei database.
import pandas as pd
data = pd.DataFrame([('Andrea', '2015', 'residenza', 'Rimini', 'via stretta 20'),
('Andrea', '2015', 'domicilio', 'Bologna', 'via larga 30'),
('Andrea', '2016', 'residenza', 'Rimini', 'via stretta 20'),
('Andrea', '2016', 'domicilio', 'Bologna', 'via larga 30'),
('Giulio', '2015', 'residenza', 'Rimini', 'via giusta 50'),
('Giulio', '2015', 'domicilio', 'Bologna', 'via falsa 40'),
('Giulio', '2016', 'residenza', 'Bologna', 'via torna 10'),
('Giulio', '2016', 'domicilio', 'Bologna', 'via torna 10'),
], columns=['nome', 'anno', 'tipologia', 'città', 'indirizzo']
)
data
La cosa importante da ricordare con questo tipo di dati (e la approfondiremo meglio la prossima lezione) è che il formato di dati per fare storage non è necessariamente il più comodo per ogni possibile analisi.
Il formato tidy è eccezionale, sopratutto per mantenere i metadati sulle mie misure, ma non sempre è il formato più comodo per l'analisi che voglio fare (ad esempio differenze fra vari momenti temporali).
Per questo motivo, tutte le librerie che lavorano sui dataframe hanno un forte focus sulla trasformazione dei ati da una forma all'altra, per permetterci di passare facilmente alla struttura dati che serve alle analisi senza sacrificare la qualità dei dati in storage.
Pandas è la libreria di python che permette la manipolazione dei dataframe.
La libreria introduce la classe DataFrame, che contiene una tabella, che contiene diverse Series, ovvero i dati in colonna che condividono lo stesso indice.
import pandas as pd
import numpy as np
Uno dei punti di forza di pandas è la capacità di leggere e scrivere praticamente qualsiasi dato di tipo tabulare.
Ad esempio possiamo caricare tutte le tabelle di una pagina wikipedia
page = 'https://en.wikipedia.org/wiki/List_of_highest-grossing_films'
wikitables = pd.read_html(page, attrs={"class":"wikitable"})
len(wikitables)
wikitables[0].head()
Le funzioni di lettura contengono decine e decine di parametri per riuscire a leggere i dati esattamente come vogliamo.
wikitables = pd.read_html(page, attrs={"class":"wikitable"}, header=0)
wikitables[0].head()
dataframe = wikitables[0].copy()
Il dataframe si comporta come una specie di dizionario.
Le chiavi sono le colonne, e ciascuna chiave fa riferimento alla Series lì contenuta
dataframe.columns
dataframe['Year'].head()
Le Series si comportano come gli array di numpy per quanto riguarda la vettorizzazione, ma lo fanno informati dall'indice della serie invece che dall'ordine degli elementi
dataframe['Year'].head() * 100
a = pd.Series([1, 2, 3], index=['a', 'b', 'c'])
a
b = pd.Series([5, 6, 7], index=['c', 'a', 'b'])
b
a+b
posso manipolare le mie colonne in molti modi, partendo dall'eliminazione di colonne non interessanti
del dataframe['Reference(s)']
dataframe.head()
dataframe.info()
dataframe['Title'].head()
dataframe.head()
Spesso e volentieri i dati reali arrivano in un formato "non ottimale".
La prima parte di qualsiasi analisi consiste nella pulizia dei dati.
Nel nostro caso ad esempio potremmo voler aggiustare il guadagno, che al momento è visto come una stringa.
c = 'Worldwide gross'
dataframe[c] = dataframe[c].str.replace(',', '')
dataframe[c] = dataframe[c].str.replace('$', '')
dataframe[c] = dataframe[c].astype(int)
dataframe.info()
Per aggiustare i dati talvolta è necessario usare la violenza...
try:
dataframe['Peak'].astype(int)
except ValueError as e:
print(e)
print(list(dataframe['Peak'].unique()))
non esiste una trasformazione semplice che possa convertire questo tipo di dati, quindi usiamo una regular expression
import re
regex = re.compile('(\d+)\D*\d*')
dataframe['Peak'] = dataframe['Peak'].str.extract(regex, expand=False)
dataframe['Peak'] = dataframe['Peak'].astype(int)
dataframe.info()
dataframe['Rank'] = dataframe['Rank'].str.extract(regex, expand=False)
dataframe['Rank'] = dataframe['Rank'].astype(int)
dataframe.info()
dataframe.describe()
%matplotlib inline
Posso farlo sia da Pandas che da Matplotlib.
Dopo vedremo una libreria più appropriata, ma queste vanno bene per un approccio quick & dirty
import pylab as plt
plt.scatter('Rank', 'Peak', data=dataframe)
dataframe.plot.scatter('Rank', 'Peak')
plt.plot('Rank', 'Worldwide gross', data=dataframe)
with plt.xkcd():
dataframe.plot('Rank', 'Worldwide gross')
plt.hist('Worldwide gross', data=dataframe);
dataframe['Worldwide gross'].plot.hist()
pandas lavora in realtà con matplotlib, per cui potete estrarre il grafico al volo e modificarlo come volete
dataframe['Worldwide gross'].plot.hist()
ax = plt.gca()
ax.set_title("Histogram of Worldwide gross", fontsize=30)
I dataframe di Pandas (ed in generale la struttura del dataframe come concetto), contiene come dati in colonna soltanto dei valori scalari, come l'altezza, il peso, e così via.
Per gestire e manipolare dati più complessi, come ad esempio una serie temporale per ogni paziente, si devono spesso creare più tabelle e collegarle logicamente fra di loro.
Una alternativa piuttosto recente sono i DataArray e DataSet delle libreria XArray.
Questi permettono di creare delle strutture molto efficienti per gestire ad esempio immagini TAC dei pazienti in più dimensioni ed in più momenti temporali e manipolarle in modo sensato (per quanto questo possa essere possibile).
Vediamo ora una serie di operazioni molto comuni sui dataframe.
Compie operazioni di tipo split-apply-combine:
wiki = "https://it.wikipedia.org/wiki/"
url_popolazione = wiki+"Comuni_d%27Italia_per_popolazione"
url_superficie = wiki+"Primi_100_comuni_italiani_per_superficie"
comuni_popolazione = pd.read_html(url_popolazione,
attrs={"class":"wikitable"},
header=0)
comuni_popolazione = comuni_popolazione[0]
comuni_popolazione.head()
comuni_superficie = pd.read_html(url_superficie,
attrs={"class":"wikitable"},
header=0)
comuni_superficie = comuni_superficie[0]
comuni_superficie.head()
comuni_superficie.groupby('Regione').mean()
g = comuni_superficie.groupby('Regione')
g.aggregate([np.mean, np.std, pd.Series.count])
comuni_superficie.groupby('Regione')['Superficie (km²)'].count()
g = comuni_superficie.groupby('Regione')['Superficie (km²)']
g.count().sort_values(ascending=False)
g = comuni_popolazione.groupby('Regione')['Abitanti']
g.count().sort_values(ascending=False)
Quando ho due tabelle distinte, che condividono una chiave, posso fare il join fra le due.
Questo mi permette di tenere le tabelle dei miei dati in forma corretta (tidy e scorrelata) e ricrearle in modo comodo per le analisi combinando più tabelle insieme
a = pd.DataFrame([('Antonio', 'M'),
('Marco', 'M'),
('Francesca', 'F'),
], columns = ['nome', 'genere'])
b = pd.DataFrame([('Antonio', 15),
('Marco', 10),
('Marco', 12),
('Marco', 23),
('Francesca', 20),
], columns = ['nome', 'spesa'])
a
b
pd.merge(a, b, on='nome')
(
pd.merge(comuni_popolazione,
comuni_superficie,
on=['Comune', 'Regione'])
).head()
Ci sono diversi modi di fare il merge, che corrispondono alle diverse combinazioni di insiemi possibili di chiavi.
sono controllati dal parametro HOW.
len(pd.merge(comuni_popolazione, comuni_superficie,
on='Comune', how='right'))
len(pd.merge(comuni_popolazione, comuni_superficie,
on='Comune', how='left'))
len(pd.merge(comuni_popolazione, comuni_superficie,
on='Comune', how='inner'))
len(pd.merge(comuni_popolazione, comuni_superficie,
on='Comune', how='outer'))
il pivoting permette di creare tavole riassuntive (incluse tavole di contingenza) a partire da una dataset tidy.
date due colonne che faranno da indice e nomi delle colonne del dataset risultante, si scelgono uno o più valori di cui fare il sommario (somma, media, conteggi, deviazione standard, etc...)
spese = [('Antonio', 'gatto', 4),
('Antonio', 'gatto', 5),
('Antonio', 'gatto', 6),
('Giulia', 'gatto', 3),
('Giulia', 'cane', 7),
('Giulia', 'cane', 8),
]
spese = pd.DataFrame(spese, columns = ['nome', 'animale', 'spesa'])
spese
pd.pivot_table(spese,
index='nome',
columns='animale',
values='spesa',
aggfunc=np.sum)
pd.pivot_table(spese,
index='nome',
columns='animale',
values='spesa',
aggfunc=np.sum,
fill_value=0)
pd.pivot_table(spese,
index='nome',
columns='animale',
values='spesa',
aggfunc=np.sum,
fill_value=0,
margins=True)
pd.pivot_table(spese,
index='nome',
columns='animale',
values='spesa',
aggfunc=pd.Series.count,
fill_value=0)
r = pd.pivot_table(spese,
index='nome',
columns='animale',
values='spesa',
aggfunc=pd.Series.count,
fill_value=0)
r = r.reset_index()
r
v = pd.melt(r, id_vars=['nome'], value_vars=['cane', 'gatto'])
v
v2 = v.set_index(['nome', 'animale'])['value']
v2
v2.unstack()
v2.unstack().stack()
v.pivot(index='nome', columns='animale', values='value')
v.pivot(index='nome', columns='animale', values='value')
è identico a
v2.unstack()
ma uno agisce sulle serie (unstack) e l'altro sui dataframe di tipo tidy (pivot)
url_amminoacidi = 'https://en.wikipedia.org/wiki/Proteinogenic_amino_acid'
info_amminoacidi = pd.read_html(url_amminoacidi, attrs={"class":"wikitable"}, header=0)
len(info_amminoacidi)
info_amminoacidi[0].head()
info_amminoacidi[1].head()
tavola1 = info_amminoacidi[0]
tavola2 = info_amminoacidi[1]
del tavola2['Short']
del tavola2['Abbrev.']
tavola = pd.merge(tavola1, tavola2, on='Amino acid')
tavola.head()
tavola.info()
tavola['Hydro- phobic'].unique()
import seaborn
Seaborn di default cambia la configurazione stardard della visualizzazione di matplotlib.
Potete impostarla come preferite grazie al modulo styles di matplotlib.
from matplotlib import style
print(sorted(style.available))
style.use('default')
seaborn.lmplot('Avg. mass (Da)',
'van der Waals volume (Å3)',
data=tavola,
hue='Hydro- phobic')
seaborn.lmplot('Avg. mass (Da)',
'van der Waals volume (Å3)',
data=tavola,
col='Hydro- phobic')
seaborn.lmplot('Avg. mass (Da)',
'van der Waals volume (Å3)',
data=tavola,
row='Hydro- phobic')
fg = seaborn.FacetGrid(data=tavola,
hue='Hydro- phobic',
size=6)
fg.map(plt.scatter,
'Avg. mass (Da)',
'van der Waals volume (Å3)',
s=50)
fg.map(seaborn.regplot,
'Avg. mass (Da)',
'van der Waals volume (Å3)',
scatter=False)
fg.add_legend();
fg = seaborn.FacetGrid(data=tavola,
col='Hydro- phobic',
hue='Hydro- phobic',
size=8,
aspect=.5)
fg.map(plt.scatter,
'Avg. mass (Da)',
'van der Waals volume (Å3)',
s=50)
fg.map(seaborn.regplot,
'Avg. mass (Da)',
'van der Waals volume (Å3)',
scatter=False)
fg.add_legend();
columns = ['Avg. mass (Da)',
'van der Waals volume (Å3)',
'pI', 'pK1 (α-COOH)',
'pK2 (α-+NH3)']
tavola[columns].head()
fd = tavola[columns+['Hydro- phobic']].dropna()
g = seaborn.PairGrid(df,
size=2,
hue='Hydro- phobic')
g.map_diag(plt.hist, alpha=0.5)
g.map_offdiag(plt.scatter, alpha=0.75, s=20);
Esaminando la tabella di wikipedia dei premi nobel in fisica, classificare quali paese abbiamo avuto il maggior numero di nobel nel corso degli anni
Per i più coraggiosi, provate a correlare la lista precendente con il consumo pro capite di birra.
Purtroppo mi sono accorto solo a posteriori che la tabella dei nobel aveva delle anomalie:
Il secondo problema si può risolvere facilmente con la decisione arbitraria di conteggiare soltanto la prima cittadinanza (la nazione di nascita, secondo quanto riportato dal sito del premio nobel).
Il primo richiede la manipolazione delle righe del dataframe, che mostro di seguito.
Come vedrete la manipolazione non è particolarmente complicata, ma richiede conoscenze che non vi ho mostrato a lezione, mi spiace.
# carico il dataset incriminato
import pandas as pd
wiki = 'https://en.wikipedia.org/wiki/'
url = wiki+'List_of_Nobel_laureates_in_Physics'
dfs = pd.read_html(url,
attrs={"class":"wikitable"},
header=0)
df = dfs[0].copy()
df.head()
# L'anno ha dei buchi dove ci sono più vincitori
# posso riempire i buchi conuna funzione 'forward-fill'
# che li riempe con il valore immediatamente precedente
df['Year'] = y.fillna(method='ffill').astype(int)
df.head()
# la parte difficile.
# per prima cosa devo fare la correzione una riga alla volta
for idx, line in df.iterrows():
# se il vincitore di quella riga è assente
# devo fare la correzione
missing_winner = pd.isnull(line['Laureate[A]'])
if missing_winner:
# creo una nuova tupla di valori ricombinando
# quella vecchia nell'ordine corretto
new_value = line[['Year', 'Country[B]', 'Rationale[C]']]
# riempo le prime 3 colonne con i valori che ho trovato
# l'argomento values mi serve a fargli ignorare
# l'indicizzazione esplicita della serie
df.loc[idx, :3] = new_value.values
df.head()
# rendiamo il dataframe un po' più piacevole allo sguardo
del df['Rationale[C]']
del df['Unnamed: 4']
df.columns = ['year', 'name', 'country']
df.head()
# controlliamo i valori unici della variabile country.
# possiamo vedere che dove c'è la doppia cittadinanza
# abbiamo un carattere speciale ('\xa0')
# che possiamo usare per dividerle
df['country'].unique()
# divido la stringa in due dove trovo il carattere speciale
t = df['country'].str.split('\xa0')
# prendo il primo pezzo di stringa
t = t.str[0]
# tolgo gli spazi bianchi
t = t.str.strip()
# la assegno di nuovo alla variabile country
df['country'] = t
# alcuni anni non hanno il country
# sono quelli in cui il nobel
# non è stato assegnato per via delle guerre
df.iloc[19:22]
# posso rimuovere le linee in cui il country è assente
df = df.dropna(subset=['country'])
# posso infine fare il mio groupby
# e vedere il risultato ordinato
gb = df.groupby('country')['year']
gb.count().sort_values(ascending=False)
Anche se l'esercizio era più difficile del previsto, sappiate che queste situazione capitano più spesso del previsto.
Ogni volta che avrete dei dati "curati" a mano, preparatevi a trovare ogni qual sorta di strani errori ed inconsistenze, che dovrete raddrizzare a colpi di codice.
In queste cose, Pandas è insostituibile (e questa è la ragione principale del suo successo)